package geotags

/*
Copyright 2021-2025 The k8gb Contributors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Generated by GoLic, for more details see: https://github.com/AbsaOSS/golic
*/

import (
	"fmt"
	"sort"
	"strings"

	"github.com/k8gb-io/k8gb/controllers/resolver"
	"github.com/k8gb-io/k8gb/controllers/utils"
	"github.com/miekg/dns"
)

// Dynamic reads geotags from parent DNS
type Dynamic struct {
	config *resolver.Config
}

func NewDynamic(config *resolver.Config) *Dynamic {
	return &Dynamic{
		config: config,
	}
}

func (p *Dynamic) GetExternalClusterNSNamesByHostname(host string) (map[string]string, error) {
	z := getZone(p.config, host)
	if z != nil {
		return p.getExternalClusterNSNamesByHostname()
	}
	return map[string]string{}, nil
}

func (p *Dynamic) getExternalClusterNSNamesByHostname() (map[string]string, error) {
	var tags []string
	var err error
	var parentNameServer *utils.DNSServer
	var found bool
	nsNames := make(map[string]string)
	// caution: This ultimately gets called during every reconciliation. Each call to dig means an interaction
	// with the DNS server. If you only have a few applications, that's fine — but if your infrastructure contains
	// thousands of GSLBs, you can overwhelm the DNS. If the DNS is overwhelmed, you need to set the external
	// GeoTags manually.
	if parentNameServer, found = p.config.ParentZoneDNSServers.Random(); !found {
		return nsNames, nil
	}
	d := p.config.DelegationZones[0]
	parentDNSServer, err := p.extractParentDNSServer(parentNameServer, d.ParentZone)
	if err != nil {
		return nsNames, fmt.Errorf("ExternalGeoTags: error extracting parent DNS servers: %w", err)
	}
	tags, err = p.getExternalTags(*parentDNSServer, d.LoadBalancedZone)
	if err != nil {
		return nsNames, fmt.Errorf("ExternalGeoTags: reading geo tags: %w", err)
	}
	for _, tag := range tags {
		nsNames[tag] = d.GetNSName(tag)
	}
	return nsNames, nil
}

// dig @extractedDNS_IP loadBalancerDomain -p edgePort NS +norec
// extracts geotags from NS records returned in ANSWER_SECTION;  gslb-ns-us-cloud.example.com -> extracts us
func (p *Dynamic) getExternalTags(edge utils.DNSServer, zone string) ([]string, error) {
	const prefix = "gslb-ns-"
	var extTags []string
	m := new(dns.Msg)
	m.SetQuestion(zone+".", dns.TypeNS)
	m.RecursionDesired = false // Equivalent to dig +norec
	c := new(dns.Client)
	r, _, err := c.Exchange(m, edge.String())
	if err != nil {
		return extTags, err
	}

	for _, ans := range r.Ns {
		ns, ok := ans.(*dns.NS)
		if !ok {
			continue
		}
		// Extract tag from NS name pattern: gslb-ns-tag-zone
		parts := strings.Split(ns.Ns, prefix)
		// filter non k8gb ns records
		if len(parts) != 2 {
			continue
		}
		// parts[0] == prefix; parts[1] == <region>-test-cloud.example.com
		tag := strings.Split(parts[1], "-")[0]
		if tag != p.config.ClusterGeoTag {
			extTags = append(extTags, tag)
		}
	}
	sort.Strings(extTags)
	return extTags, nil
}

// dig @edgeDNS parentDomain -p edgePort NS
// extract first A record from ADDITIONAL SECTION
func (p *Dynamic) extractParentDNSServer(parentNameServer *utils.DNSServer, parentZone string) (*utils.DNSServer, error) {
	m := new(dns.Msg)
	m.SetQuestion(parentZone+".", dns.TypeNS)
	c := new(dns.Client)
	r, _, err := c.Exchange(m, parentNameServer.String())
	if err != nil {
		return nil, err
	}
	for _, ans := range r.Extra {
		a, ok := ans.(*dns.A)
		if !ok {
			continue
		}
		return &utils.DNSServer{Host: a.A.String(), Port: parentNameServer.Port}, nil
	}
	return nil, fmt.Errorf("GeoTags: error extracting parent DNS servers: %w", err)
}
